
var gl;
var canvas;
var model;

// on page load call webGLStart()
$(document).ready(function() {
	model = new DataModel();
	model.generateTestingData();

	// update html for initially selected data point
	updateHTMLForSelectedDataPoint(
		model.getSelectedDataPointXIndexName(),
		model.getSelectedDataPointYValue(),
		model.getSelectedDataPointZIndexName());	

	webGLStart();
});

// ##################
// initialises web GL and starts the render loop
function webGLStart() {

	// get canvas
	canvas = document.getElementById("Canvas3D");
	
	var glInit = initGL(canvas);
	var shInit = initShaders();
	var buInit = initBuffers();
	
	// if error, don't start render loop
	if(!glInit || !shInit || !buInit) {
		return false;
	}

	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.enable(gl.DEPTH_TEST);

	tick();
}

// ##################
// get gl object and set it's viewport dimensions
function initGL(canvas) {
	try {
		// set up WebGL
		gl = WebGLUtils.setupWebGL(canvas, {antialias: true});
		gl.viewportWidth = canvas.width;
		gl.viewportHeight = canvas.height;
		
		if(!gl) {
			throw new Error("WebGLUtils.setupWebGL failed");
		}
		
	} catch (e) {
		alert("Could not initialise WebGL, sorry :-(. "+ e.message);
		return false;
	}
	
	return true;
}

// ##################
// get shader from gl for shader source given in element with id given
function getShader(gl, id) {
	var shaderScript = document.getElementById(id);
	if (!shaderScript) {
		return null;
	}

	var str = "";
	var k = shaderScript.firstChild;
	while (k) {
		if (k.nodeType == 3) {
			str += k.textContent;
		}
		k = k.nextSibling;
	}

	var shader;
	if (shaderScript.type == "x-shader/x-fragment") {
		shader = gl.createShader(gl.FRAGMENT_SHADER);
	} else if (shaderScript.type == "x-shader/x-vertex") {
		shader = gl.createShader(gl.VERTEX_SHADER);
	} else {
		return null;
	}

	gl.shaderSource(shader, str);
	gl.compileShader(shader);

	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
		alert(gl.getShaderInfoLog(shader));
		return null;
	}

	return shader;
}


var shaderProgram;

// ##################
// inits shaders
function initShaders() {
	// make shaders based on the two definitions
	var fragmentShader = getShader(gl, "shader-fs");
	var vertexShader = getShader(gl, "shader-vs");

	// set up shaders
	shaderProgram = gl.createProgram();
	gl.attachShader(shaderProgram, vertexShader);
	gl.attachShader(shaderProgram, fragmentShader);
	gl.linkProgram(shaderProgram);

	if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
		alert("Could not initialise shaders");
		return false;
	}

	gl.useProgram(shaderProgram);

	shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
	gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
	
	shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
	gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

	shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
	shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
	
	return true;
}


var mvMatrix = mat4.create();
var mvMatrixStack = [];
var pMatrix = mat4.create();

// ##################
// push new model view matrix onto stack
function mvPushMatrix() {
	var copy = mat4.create();
	mat4.set(mvMatrix, copy);
	mvMatrixStack.push(copy);
}

// ##################
// pop current model view matrix off stack
function mvPopMatrix() {
	if (mvMatrixStack.length == 0) {
		throw "Invalid popMatrix!";
	}
	mvMatrix = mvMatrixStack.pop();
}

// ##################
function setMatrixUniforms() {
	gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
	gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}		
	
// ##################
function degToRad(degrees) {
	return degrees * Math.PI / 180;
}


var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubeVertexPositionBuffer;
var cubeVertexColorBuffer_yellow;
var cubeVertexColorBuffer_red;
var cubeVertexColorBuffer_brightRed;
var cubeVertexIndexBuffer;

// ##################
// init the buffers
function initBuffers() {
	pyramidVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(getVertices_Pyramid()), gl.STATIC_DRAW);
	pyramidVertexPositionBuffer.itemSize = 3;
	pyramidVertexPositionBuffer.numItems = 12;

	pyramidVertexColorBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
	var colors = [
		// Front face
		1.0, 0.0, 0.0, 1.0,
		0.0, 1.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,

		// Right face
		1.0, 0.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,
		0.0, 1.0, 0.0, 1.0,

		// Back face
		1.0, 0.0, 0.0, 1.0,
		0.0, 1.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,

		// Left face
		1.0, 0.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,
		0.0, 1.0, 0.0, 1.0
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	pyramidVertexColorBuffer.itemSize = 4;
	pyramidVertexColorBuffer.numItems = 12;

	cubeVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(getVertices_Cube()), gl.STATIC_DRAW);
	cubeVertexPositionBuffer.itemSize = 3;
	cubeVertexPositionBuffer.numItems = 24;

	// yellow
	cubeVertexColorBuffer_yellow = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_yellow);
	colors = generateCubeColors([1.2, 1.0, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_yellow.itemSize = 4;
	cubeVertexColorBuffer_yellow.numItems = 24;

	// red
	cubeVertexColorBuffer_red = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_red);
	colors = generateCubeColors([1.2, 0.55, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_red.itemSize = 4;
	cubeVertexColorBuffer_red.numItems = 24;
	
	// bright red
	cubeVertexColorBuffer_brightRed = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_brightRed);
	colors = generateCubeColors([0.55, 1.2, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_brightRed.itemSize = 4;
	cubeVertexColorBuffer_brightRed.numItems = 24;
	
	cubeVertexIndexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	var cubeVertexIndices = [
		0,   1,  2,      0,  2,  3, // Front face
		4,   5,  6,      4,  6,  7, // Back face
		8,   9, 10,      8, 10, 11, // Top face
		12, 13, 14,     12, 14, 15, // Bottom face
		16, 17, 18,     16, 18, 19, // Right face
		20, 21, 22,     20, 22, 23  // Left face
	];
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
	cubeVertexIndexBuffer.itemSize = 1;
	cubeVertexIndexBuffer.numItems = 36;
	
	return true;
}

var rotationPyramid = 0;
var rotationCube = 0;


// ##################
// draw scene
function drawScene() {

	gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
	
	gl.clear(
		gl.COLOR_BUFFER_BIT
		| gl.DEPTH_BUFFER_BIT);

	// set perspective
	mat4.perspective(
		45,                                   // vertical field of view (in degrees)
		gl.viewportWidth / gl.viewportHeight, // width:height ratio of canvas
		0.1,                                  // near clip (in units from viewpoint)
		100.0,                                // far clip  (in units from viewpoint)
		pMatrix);                             // projection matrix

	// --------------
	// center mv matrix
	mat4.identity(mvMatrix);                       // identity matrix

	// --------------
	// push for pyramid and cube position
	mvPushMatrix();
	mat4.translate(mvMatrix, [7.0, -4.0, -16.0]);   // translate pyramid and cube location

	// --------------
	// pyramid
	
	mvPushMatrix();
	
	mat4.rotate(mvMatrix, degToRad(rotationPyramid), [0, 1, 0]); // rotate pyramid around y axis

	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

	setMatrixUniforms();
//	gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);
	gl.drawArrays(gl.LINE_LOOP, 0, pyramidVertexPositionBuffer.numItems);

	mvPopMatrix();

	// --------------
	// cube

	mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);    // move position x += 3

	mvPushMatrix();
	mat4.rotate(mvMatrix, degToRad(rotationCube), [1, 1, 1]); // rotate cube on all axis

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_yellow);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer_yellow.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	setMatrixUniforms();
	gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

	mvPopMatrix();

	// --------------
	// pop back to indentity
	mvPopMatrix();

	// --------------
	// push, position, and rotate for graph x=0, y=0 position
	mvPushMatrix();
	mat4.translate(mvMatrix, [-15.0, -4.0, -25.0]);   // translate
	mat4.rotate(mvMatrix, degToRad(INITIAL_GRAPH_X_ROTATION), [1, 0, 0]); // rotate graph so looking down on it
	mat4.rotate(mvMatrix, degToRad(INITIAL_GRAPH_Y_ROTATION), [0, -1, 0]); // rotate graph so looking along it

	// --------------
	// graph data

	var data = model.getDataPoints();

	// for each data point
	for(i in data) {
	
		mvPushMatrix(); // push for data element
		
		// translate for x and z index position
		mat4.translate(
			mvMatrix, 
			[
				(BAR_X_SPACING + BAR_X_WIDTH) * data[i][0], // x spacing * x pos
				data[i][1] * 0.5,                           // y height / 2
				-(BAR_Z_SPACING + BAR_Z_WIDTH) * data[i][2] // z spacing * z pos
			]);

		// scale for data element's y value
		mat4.scale(
			mvMatrix, 
			[
				BAR_X_WIDTH * 1,          // x width
				data[i][1],  // y height
				BAR_Z_WIDTH * 1           // z width
			]);



		gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
		gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

		var isSelectedOnXAxis = false;
		var isSelectedOnZAxis = false;
		if(data[i][0] == model.getSelectedXIndex()) { // if selected on x axis point
			isSelectedOnXAxis = true;
		}
		if(data[i][2] == model.getSelectedZIndex()) { // if selected on z axis point
			isSelectedOnZAxis = true;
		}
		
		if(isSelectedOnZAxis) {
			if(isSelectedOnXAxis) { // if selected on z and x
				gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_brightRed);
				gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer_brightRed.itemSize, gl.FLOAT, false, 0, 0);
			} else { // if just selected on z
				gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_red);
				gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer_red.itemSize, gl.FLOAT, false, 0, 0);
			}
		} else {
			gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_yellow);
			gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer_yellow.itemSize, gl.FLOAT, false, 0, 0);
		}

		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
		setMatrixUniforms();
		gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

		mvPopMatrix();
	}
	
	
	
}


var lastTime = 0;
var timeOfLastFramerateUpdate = 0;

// ##################
function animate() {

	var timeNow = new Date().getTime();

	// if not first animate
	if (lastTime != 0) {
		// get amount of time elapsed since last animate
		var elapsed = timeNow - lastTime;

		// if it's been over 0.3 second since we updated framrate
		if(timeNow - timeOfLastFramerateUpdate > 300) {

			timeOfLastFramerateUpdate = timeNow;

			// update framerate displayed
			updateHTMLForFramerate(Math.floor(1000/elapsed));
		}

		// update for animations
		rotationPyramid += (90 * elapsed) / 1000.0;
		rotationCube -= (75 * elapsed) / 1000.0;
		
		
	}
	lastTime = timeNow;
}


// ##################
function tick() {
	requestAnimFrame(tick,canvas);
	drawScene();
	animate();
}


